var EXPORTED_SYMBOLS = ["MLDonkeyClient"];

Components.utils.import("resource://mldonkeyff/MLDonkeyUtils.js");
Components.utils.import("resource://mldonkeyff/MLDonkeyProtocolTypes.js");
Components.utils.import("resource://mldonkeyff/MLDonkeyConstants.js");
Components.utils.import("resource://mldonkeyff/MLDonkeyProtocol.js");

function MLDonkeyClient(_hostname, _port, _username, _password) {
	// Connection data
	var hostname = _hostname;
	var port = _port;
	var username = _username;
	var password = _password;
	var connected = false;

	this.getHostname = function() {
		return hostname;
	};

	this.getPort = function() {
		return port;
	};

	this.getUsername = function() {
		return username;
	};
	
	this.getPassword = function() {
		return password;
	};
	
	this.isConnected = function() {
		return connected;
	}
	
	this.setConnected = function(_connected) {
		connected = _connected;
	}
	
	// Internal data
	var mldonkeyData = new MLDonkeyData();

	this.getMLDonkeyData = function() {
		return mldonkeyData;
	};
	
	var protocolImpl = new MLDonkeyProtocol(this);
	
	this.getProtocolImpl = function() {
		return protocolImpl;
	};
	
	// Attributes for socket
	var transportService = Components.classes["@mozilla.org/network/socket-transport-service;1"].getService(Components.interfaces.nsISocketTransportService);
	var transport = null;
	
	this.getTransportService = function() {
		return transportService;
	};
	
	// Send/Receive Cache
	this.sendCache = new Array();
	this.recvCache = new Array();
	
	// 
	this.observer = {
		incompleteMessage: false,
		incompleteMessageLen: 0,
		onStartRequest : function(aRequest, aContext) {
		},
		onStopRequest : function(aRequest, aContext, aStatusCode) {
		},
		onDataAvailable : function(request, context, inputStream, offset, count) {
			var msgLen = null;
			var opCode = null;
			var fn = null;
			try {
				var readed = this.parent.bistream.readBytes(count);
				var currLen = this.parent.recvCache.length;
	
				// this.parent.recvCache = this.parent.recvCache.concat(readed);
				for ( var i = 0; i < count; i++) {
					this.parent.recvCache[currLen + i] = readed[i];
				}
				
				/*MLDonkeyUtils.getPromptService().alert(null, "MLDonkeyClient", "Prima di readMessageLen(): count: " + count + " readed: " + readed + " this.parent.recvCache: " + this.parent.recvCache
				    + "\r\nthis.parent.recvCache[0]: " + this.parent.recvCache[0].charCodeAt(0)  
				    + "\r\nthis.parent.recvCache[1]: " + this.parent.recvCache[1].charCodeAt(0)  
				    + "\r\nthis.parent.recvCache[2]: " + this.parent.recvCache[2].charCodeAt(0)  
				    + "\r\nthis.parent.recvCache[3]: " + this.parent.recvCache[3].charCodeAt(0)
				    + "\r\nthis.parent.recvCache[4]: " + this.parent.recvCache[4].charCodeAt(0)
				    + "\r\nthis.parent.recvCache[5]: " + this.parent.recvCache[5].charCodeAt(0)
				    );*/

				while(this.parent.recvCache.length > 4) {
					dump("MLDonkeyClient: " + "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\r\n");
					if(!this.incompleteMessage) {
						incompleteMessageLen = this.parent.readMessageLen();
					    dump("MLDonkeyClient: " + "Leggo la lunghezza del messaggio..." + incompleteMessageLen + " mentre quella del buffer e' " + this.parent.recvCache.length + "\r\n");
					}
					
					if(incompleteMessageLen > this.parent.recvCache.length) {
					    dump("MLDonkeyClient: " + "Il msgLen è maggiore di this.parent.recvCache.length! this.parent.recvCache.length: " + this.parent.recvCache.length + " incompleteMessageLen: " + incompleteMessageLen + " opCode previsto: " + ( ((this.parent.recvCache[1].charCodeAt(0) & 0xFF) << (1 * 8)) & ((this.parent.recvCache[0].charCodeAt(0) & 0xFF) << (0 * 8)) )+ "\r\n");
						this.incompleteMessage = true;
					    dump("MLDonkeyClient: " + "Ritorno e aspetto il prossimo pacchetto\r\n");
						break;
					}
					
					this.incompleteMessage = false;
					msgLen = incompleteMessageLen;
					
				    dump("MLDonkeyClient: " + "Messaggio completo ("); for(var i=0; i<msgLen; i++) dump(this.parent.recvCache[i].charCodeAt(0) + ","); dump(")\r\n");
				    
					// if(msgLen > count) MLDonkeyUtils.getPromptService().alert(null, "MLDonkeyClient", "BOIAMONDO!!! offset: " + offset + " count: " + count + " msgLen: " + msgLen);
					// MLDonkeyUtils.getPromptService().alert(null, "MLDonkeyClient", "BOIAMONDO!!! offset: " + offset + " count: " + count);
	
					opCode = this.parent.readOpCode();
					dump("MLDonkeyClient: " + "Leggo l'opCode... " + this.parent.getProtocolImpl().getRecvProtocolOpCodeToLiteral(opCode) + "\r\n");
					
					// MLDonkeyUtils.getPromptService().alert(null, "MLDonkeyClient", "msgLen: " + msgLen + "\n\ropCode: " + opCode);
					
					fn = this.parent.getProtocolImpl().getRecvProtocolFunctionFromOpCode(opCode);
					
					// MLDonkeyUtils.getPromptService().alert(null, "MLDonkeyClient", "count: " + count + "\r\nmsgLen: " + msgLen + "\r\nopCode: " + opCode + "\r\nfn: " + fn + "\r\nCore protocol: " + this.parent.mldonkeyData.protocolData.server + "");
					
					try {
						fn.call(this.parent.getProtocolImpl());
					} catch(err) {
						dump("MLDonkeyClient: " + "Errore durante la chiamata del metodo di protocollo... " + err + "\r\n");
						if(err == "TODO") {
							this.parent.recvCache = this.parent.recvCache.slice(msgLen - 2 /* size of opCode */);
						} else {
							dump("MLDonkeyClient: " + "I dati erano..."); for(var i=0; i<this.parent.recvCache.length; i++) dump(this.parent.recvCache[i].charCodeAt(0) + ","); dump("\r\n");
						}
					}
					
					dump("MLDonkeyClient: " + "Chiamo il metodo del protocollo... FINITO\r\n");
					dump("MLDonkeyClient: " + "-------------------------------------------------------------------------------\r\n");
				}
				
				// MLDonkeyUtils.getPromptService().alert(null, "MLDonkeyClient", "count: " + count + "\r\nmsgLen: " + msgLen + "\r\nopCode: " + opCode + "\r\nfn: " + fn + "\r\nmldonkeyData.protocolData.server: " + this.parent.mldonkeyData.protocolData.server + "");
				
//				// TODO : Log it :
//				if(this.parent.recvCache.length > 0) {
//					// MLDonkeyUtils.getPromptService().alert(null, "MLDonkeyClient", "Unexpected trailing data on opCode " + opCode + " (data: " + this.parent.recvCache + ")");
//					dump("MLDonkeyClient: " + "Unexpected trailing data on opCode " + opCode + " (data: ");
//					for(var i=0; i<this.parent.recvCache.length; i++) {
//						dump(this.parent.recvCache[i].charCodeAt(0) + ",");
//					}
//					dump(")\r\n");
//				}
//				this.parent.recvCache = new Array();
			} catch (err) {
				// TODO : To reactivate :
				/* if(err == "TODO") */ //MLDonkeyUtils.getPromptService().alert(null, "MLDonkeyClient", "Error on data read (count: " + count + " msgLen: " + msgLen + " opCode: " + opCode + " err: " + err + ")");
				dump("MLDonkeyClient: " + "Error on data read (count: " + count + " msgLen: " + msgLen + " opCode: " + opCode + " err: " + err + ")\r\n");
			}// finally {
				// Reset the buffer
				//this.parent.recvCache = new Array();
			//}
		}
	};
	
	// Reference to parent
	this.observer.parent = this;
	
	// Methods ///////////////////////////////////////////////////////////////////////////////////////////////////////////
	
	// Receive Cache Handler
	this.readBytes = function(numOfBytes) {
		var ret = this.recvCache.slice(0, numOfBytes);
		this.recvCache = this.recvCache.slice(numOfBytes);
		return ret;
	};
	
	this.readIntInNetworkOrder = function(numOfBytes) {
		var myArray = this.readBytes(numOfBytes);
		var ret = 0;
		for ( var i = 0; i < numOfBytes; i++) {
			ret |= ((myArray[i].charCodeAt(0) & 0xFF) << (i * 8));
		}
		return ret;
	};
	
	this.readInt8 = function() {
		return this.readIntInNetworkOrder(1);
	};
	
	this.readInt16 = function() {
		return this.readIntInNetworkOrder(2);
	};
	
	this.readInt32 = function() {
		return this.readIntInNetworkOrder(4);
	};
	
	this.readInt64 = function() {
		return this.readIntInNetworkOrder(8);
	};
	
	this.readString = function() {
		var strLen = this.readInt16();
		var myArray = this.readBytes(strLen);
		var ret = new String();
		for ( var i = 0; i < strLen; i++) {
			ret += myArray[i];
		}
		return ret;
	};
	
	this.readArrayOfChar = function(length) {
		var myArray = this.readBytes(length);
		var ret = new String();
		for(var i = 0; i < length; i++) {
			ret += myArray[i];
		}
		return ret;
	};

	this.readBoolean = function() {
		var int8 = this.readInt8();
		return int8 == 0 ? false : true;
	};
	
	/**
	 * TODO
	 */
	this.readFloat = function() {
		// var strFloat = this.readString();
		// return parseFloat(strFloat);
		throw "TODO : this.readFloat";
	};
	
	this.readList = function(dataType) {
		var ret = new Array();
		var numOfElement = this.readInt16();
		var fn = null;
		//
		switch (dataType) {
			case MLDonkeyConstants.INT8_TYPE:
				fn = this.readInt8;
				break;
			case MLDonkeyConstants.INT16_TYPE:
				fn = this.readInt16;
				break;
			case MLDonkeyConstants.INT32_TYPE:
				fn = this.readInt32;
				break;
			case MLDonkeyConstants.STRING_TYPE:
				fn = this.readString;
				break;
			case MLDonkeyConstants.BOOLEAN_TYPE:
				fn = this.readBoolean;
				break;
			case MLDonkeyConstants.FLOAT_TYPE:
				fn = this.readFloat;
				break;
			default:
				throw "Type not supported";
		}
		//
		for ( var i = 0; i < numOfElement; i++) {
			ret[i] = fn.call(this);
		}
		return ret;
	};
	
	/**
	 * TODO
	 */
	this.readRecords = function() {
		throw "this.readRecords : TODO";
	};
	
	/**
	 * Copied from <code>this.readString</code>.
	 */
	this.readMd4 = function() {
		var strLen = 16;
		var myArray = this.readBytes(strLen);
		var ret = "";
		for ( var i = 0; i < strLen; i++) {
			ret[i] = String.fromCharCode(myArray[i]);
		}
		return ret;
	};
	
	this.readMessageLen = function() {
		return this.readInt32();
	};
	
	this.readOpCode = function() {
		return this.readInt16();
	};
	
	// Send Cache Handler
	this.sendMessage = function(opCode) {
		dump("MLDonkeyClient: ");
		// Reset the buffer
		this.sendCache = new Array();
		// Write opCode
		
		dump("Invio l'opcode " + this.getProtocolImpl().getSendProtocolOpCodeToLiteral(opCode) + "... ");
		
		this.writeOpCode(opCode)
		// Invoke protocol function
		var fn = this.getProtocolImpl().getSendProtocolFunctionFromOpCode(opCode);
		fn.call(null);
		//
		var messageWithoutLength = this.sendCache;
		this.sendCache = new Array();
		// Add length of the message
		this.writeMessageLen(messageWithoutLength.length);
		this.writeBytes(messageWithoutLength);
		//
		this.bostream.writeByteArray(this.sendCache, this.sendCache.length);

		dump("FINITO!\r\n");
	};
	
	this.writeBytes = function(bytes) {
		var cacheLen = this.sendCache.length;
		var bytesLen = bytes.length;
		for ( var i = 0; i < bytesLen; i++) {
			this.sendCache[cacheLen + i] = bytes[i];
		}
	};
	
	this.writeIntInNetworkOrder = function(number, numOfBytes) {
		var bytes = new Array();
		for ( var i = 0; i < numOfBytes; i++) {
			bytes[i] = ((number >> (i * 8)) & 0xFF);
		}
		//
		this.writeBytes(bytes);
	};
	
	this.writeInt8 = function(number) {
		this.writeIntInNetworkOrder(number, 1);
	};
	
	this.writeInt16 = function(number) {
		this.writeIntInNetworkOrder(number, 2);
	};
	
	this.writeInt32 = function(number) {
		this.writeIntInNetworkOrder(number, 4);
	};
	
	this.writeInt64 = function(number) {
		this.writeIntInNetworkOrder(number, 8);
	};
	
	this.writeString = function(string) {
		var strLen = string.length;
		var bytes = new Array();
		for ( var i = 0; i < strLen; i++) {
			bytes[i] = string.charCodeAt(i);
		}
		this.writeInt16(strLen);
		this.writeBytes(bytes);
	};
	
	this.writeArrayOfChar = function(string) {
		var strLen = string.length;
		var bytes = new Array();
		for ( var i = 0; i < strLen; i++) {
			bytes[i] = string.charCodeAt(i);
		}
		this.writeBytes(bytes);
	};
	
	this.writeBoolean = function(boolean) {
		this.writeInt8(boolean == true ? 1 : 0);
	};
	
	/**
	 * TODO
	 */
	this.writeFloat = function(float) {
		// var strFloat = this.readString();
		// return parseFloat(strFloat);
		throw "TODO : this.writeFloat";
	};
	
	this.writeMessageLen = function(messageLen) {
		return this.writeInt32(messageLen);
	};
	
	this.writeOpCode = function(opCode) {
		return this.writeInt16(opCode);
	};
	
	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	this.getIdentifier = function() {
		return MLDonkeyClient.createIdentifier(this.getHostname(), this.getPort(), this.getUsername());
	};

	this.connect = function() {
		try {
			this.transport = this.getTransportService().createTransport(null, 0, this.getHostname(), this.getPort(), null);
			
			this.istream = this.transport.openInputStream(Components.interfaces.nsITransport.OPEN_UNBUFFERED, 0, 0);
			this.ostream = this.transport.openOutputStream(Components.interfaces.nsITransport.OPEN_BLOCKING, 0, 0);
			
			this.bistream = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream);
			this.bistream.setInputStream(this.istream);
			
			this.bostream = Components.classes["@mozilla.org/binaryoutputstream;1"].createInstance(Components.interfaces.nsIBinaryOutputStream);
			this.bostream.setOutputStream(this.ostream);
			
			this.ipump = Components.classes["@mozilla.org/network/input-stream-pump;1"].createInstance(Components.interfaces.nsIInputStreamPump);
			this.ipump.init(this.istream, -1, -1, 0, 0, true);
			
			this.sendMessage(MLDonkeyProtocol.SEND_COREPROTOCOL);
			
			this.ipump.asyncRead(this.observer, null);
		} catch (err) {
			MLDonkeyUtils.getPromptService().alert(null, "MLDonkeyClient", "Error on connection (" + err + ")");
		}
	};

	this.disconnect = function() {
		if(this.isConnected()) {
			this.bostream.close();
			
			this.istream.close();
			this.ostream.close();
			
			this.transport.close(/* ??? */ 0);
			
			this.setConnected(false);
		}
	};
}

// Static methods
// ////////////////////////////////////////////////////////////////////////////////////////////////////

MLDonkeyClient.createIdentifier =  function(hostname, port, username) {
	return "mldonkey://" + username + "@" + hostname + ":" + port;
}
